{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Model compression using channel pruning\n",
    "\n",
    "This notebook contains a working example of AIMET model compression using the channel pruning technique. \n",
    "\n",
    "Channel pruning aims to reduce computational MACS or memory requirements of the model. After applying channel pruning, the compressed model must be fine-tuned (meaning trained again for a few epochs) to restore accuracy to a level near the original model's.\n",
    "\n",
    "See [Model Compression](/user_guide/model_compression) for a detailed discussion of channel pruning and other compression techniques.\n",
    "\n",
    "## Overall flow\n",
    "\n",
    "The example follows these high-level steps:\n",
    "\n",
    "1. Instantiate the evaluation and training pipeline\n",
    "2. Load the model and evaluate it to find the baseline accuracy\n",
    "3. Compress the model and fine-tune:  \n",
    "   1. Compress the model using channel pruning and calculate post-compression accuracy  \n",
    "   2. Fine-tune the model\n",
    "\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "This notebook does not show state-of-the-art results. For example, it uses a relatively quantization-friendly model (Resnet18). Also, some optimization parameters like number of fine-tuning epochs are chosen to improve execution speed in the notebook.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Dataset\n",
    "\n",
    "This example does image classification on the ImageNet dataset. If you already have a version of the data set, use that. Otherwise download the data set, for example from https://image-net.org/challenges/LSVRC/2012/index .\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "The dataloader provided in this example relies on these features of the ImageNet data set:\n",
    "\n",
    "- Subfolders `train` for the training samples and `val` for the validation samples. See the [pytorch dataset description](https://pytorch.org/vision/0.8/_modules/torchvision/datasets/imagenet.html) for more details.\n",
    "- One subdirectory per class, and one file per image sample.\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "To speed up the execution of this notebook, you can use a reduced subset of the ImageNet dataset. For example: The entire ILSVRC2012 dataset has 1000 classes, 1000 training samples per class and 50 validation samples per class. However, for the purpose of running this notebook, you can reduce the dataset to, say, two samples per class.\n",
    "\n",
    "</div>\n",
    "\n",
    "Edit the cell below to specify the directory where the downloaded ImageNet dataset is saved."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DATASET_DIR = '/path/to/dataset/'         # Replace this path with a real directory"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 1. Instantiate the example training and validation pipeline\n",
    "\n",
    "**Use the following training and validation loop for the image classification task.**\n",
    "\n",
    "Things to note:\n",
    "\n",
    "- AIMET does not put limitations on how the training and validation pipeline is written. AIMET modifies the user's model to create a QuantizationSim model, which is still a PyTorch model. The QuantizationSim model can be used in place of the original model when doing inference or training.\n",
    "- AIMET doesn not put limitations on the interface of the `evaluate()` or `train()` methods. You should be able to use your existing evaluate and train routines as-is.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import torch\n",
    "from typing import List\n",
    "from Examples.common import image_net_config\n",
    "from Examples.torch.utils.image_net_evaluator import ImageNetEvaluator\n",
    "from Examples.torch.utils.image_net_trainer import ImageNetTrainer\n",
    "from Examples.torch.utils.image_net_data_loader import ImageNetDataLoader\n",
    "\n",
    "class ImageNetDataPipeline:\n",
    "\n",
    "    @staticmethod\n",
    "    def get_val_dataloader() -> torch.utils.data.DataLoader:\n",
    "        \"\"\"\n",
    "        Instantiates a validation dataloader for ImageNet dataset and returns it\n",
    "        \"\"\"\n",
    "        data_loader = ImageNetDataLoader(DATASET_DIR,\n",
    "                                         image_size=image_net_config.dataset['image_size'],\n",
    "                                         batch_size=image_net_config.evaluation['batch_size'],\n",
    "                                         is_training=False,\n",
    "                                         num_workers=image_net_config.evaluation['num_workers']).data_loader\n",
    "        return data_loader\n",
    "\n",
    "    @staticmethod\n",
    "    def evaluate(model: torch.nn.Module, iterations: int, use_cuda: bool) -> float:\n",
    "        \"\"\"\n",
    "        Given a torch model, evaluates its Top-1 accuracy on the dataset\n",
    "        :param model: the model to evaluate\n",
    "        :param iterations: the number of batches to be used to evaluate the model. A value of 'None' means the model will be\n",
    "                           evaluated on the entire dataset once.\n",
    "        :param use_cuda: whether or not the GPU should be used.\n",
    "        \"\"\"\n",
    "        evaluator = ImageNetEvaluator(DATASET_DIR, image_size=image_net_config.dataset['image_size'],\n",
    "                                      batch_size=image_net_config.evaluation['batch_size'],\n",
    "                                      num_workers=image_net_config.evaluation['num_workers'])\n",
    "\n",
    "        return evaluator.evaluate(model, iterations=iterations, use_cuda=use_cuda)\n",
    "\n",
    "    @staticmethod\n",
    "    def finetune(model: torch.nn.Module, epochs: int, learning_rate: float, learning_rate_schedule: List, use_cuda: bool):\n",
    "        \"\"\"\n",
    "        Given a torch model, finetunes the model to improve its accuracy\n",
    "        :param model: the model to finetune\n",
    "        :param epochs: The number of epochs used during the finetuning step.\n",
    "        :param learning_rate: The learning rate used during the finetuning step.\n",
    "        :param learning_rate_schedule: The learning rate schedule used during the finetuning step.\n",
    "        :param use_cuda: whether or not the GPU should be used.\n",
    "        \"\"\"\n",
    "        trainer = ImageNetTrainer(DATASET_DIR, image_size=image_net_config.dataset['image_size'],\n",
    "                                  batch_size=image_net_config.train['batch_size'],\n",
    "                                  num_workers=image_net_config.train['num_workers'])\n",
    "\n",
    "        trainer.train(model, max_epochs=epochs, learning_rate=learning_rate,\n",
    "                      learning_rate_schedule=learning_rate_schedule, use_cuda=use_cuda)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 2. Load the model and evaluate to get a baseline FP32 accuracy score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**2.1 Load a pretrained resnet18 model from torchvision.** \n",
    "\n",
    "You can load any pretrained PyTorch model instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from torchvision.models import resnet18\n",
    "\n",
    "model = resnet18(pretrained=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "**2.2 Decide whether to place the model on a CPU or CUDA device.** \n",
    "\n",
    "This example uses CUDA if it is available. You can change this logic and force a device placement if needed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "use_cuda = False\n",
    "if torch.cuda.is_available():\n",
    "    use_cuda = True\n",
    "    model.to(torch.device('cuda'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "**2.3 Compute the floating point 32-bit (FP32) accuracy of this model using the evaluate() routine.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "accuracy = ImageNetDataPipeline.evaluate(model, iterations=None, use_cuda=use_cuda)\n",
    "print(accuracy)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Compress the model and fine-tune\n",
    "\n",
    " **3.1. Compress the model using channel pruning and evaluate it to find post-compression accuracy.**\n",
    "\n",
    "Use AIMET to define compression parameters for channel pruning.\n",
    "\n",
    "Some key parameters:\n",
    "\n",
    "- **target_comp_ratio**: The desired compression ratio for Channel Pruning. This example uses 0.9 to compress the model by 10%.\n",
    "- **num_comp_ratio_candidates**: Defines how many compression ratios to try. To calculate how compressible each layer is, AIMET tried difference compression ratios. The value three causes AIMET to try values of 0.33, 0.66, and 1.00. Higher values result in more granular measurements but take longer to complete. In practice a good compromise is 10. \n",
    "- **modules_to_ignore**: A list that contains the references of model layers to ignore during compression. This example adds the first layer for illustration purposes. Other layers can be added if desired.\n",
    "- **mode**: **Auto** mode performs per-layer compressibility analysis and calculates how much to compress each layer. The alternative is **Manual**.\n",
    "- **data_loader**: The data loader that channel pruning uses to load unlabelled data samples for the layer-by-layer reconstruction procedure. You can pass your existing validation or training-set loader.\n",
    "- **num_reconstruction_samples**: The number of samples used in the layer-by-layer reconstruction procedure. the example value of 10 here was chosen for quick execution but is too low for real compression. A typical \"real\" setting is about 1000 samples.\n",
    "- **allow_custom_downsample_ops**: If enabled, AIMET channel pruning inserts downsample ops into the model graph if needed, enabling more convolutional layers to be considered for pruning at the cost of memory overhead. We suggest disabling this by default.\n",
    "- **eval_callback**: The model evaluation function. The expected signature of the evaluate function is `<function_name>(model, eval_iterations, use_cuda)` The function should return an accuracy metric.\n",
    "- **eval_iterations**: The number of batches of data with which to evaluate the model during compression. The example uses one for execution speed. In practice, use a high enough number to give good accuracy results. The eval callback is expected to use the same samples for every invocation of the callback.\n",
    "- **compress_scheme**: The 'channel pruning' compression scheme.\n",
    "- **cost_metric**: Set to target either MACs or memory for reduction by the desired compression ratio. The example chooses 'mac'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from decimal import Decimal\n",
    "from aimet_torch.defs import GreedySelectionParameters, ChannelPruningParameters\n",
    "from aimet_common.defs import CompressionScheme, CostMetric\n",
    "\n",
    "greedy_params = GreedySelectionParameters(target_comp_ratio=Decimal(0.9),\n",
    "                                          num_comp_ratio_candidates=3)\n",
    "modules_to_ignore = [model.conv1]\n",
    "auto_params = ChannelPruningParameters.AutoModeParams(greedy_select_params=greedy_params,\n",
    "                                                      modules_to_ignore=modules_to_ignore)\n",
    "data_loader = ImageNetDataPipeline.get_val_dataloader()\n",
    "params = ChannelPruningParameters(data_loader=data_loader,\n",
    "                                  num_reconstruction_samples=10,\n",
    "                                  allow_custom_downsample_ops=False,\n",
    "                                  mode=ChannelPruningParameters.Mode.auto,\n",
    "                                  params=auto_params)\n",
    "\n",
    "eval_callback = ImageNetDataPipeline.evaluate\n",
    "eval_iterations = 1\n",
    "compress_scheme = CompressionScheme.channel_pruning\n",
    "cost_metric = CostMetric.mac"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "**3.2 Call the AIMET ModelCompressor.compress_model API using the above parameters.** \n",
    "\n",
    "This call returns a compressed model and relevant statistics. The ModelCompressor evaluates the model while compressing using the evaluation function from the data pipeline.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "from aimet_torch.compress import ModelCompressor\n",
    "compressed_model, comp_stats = ModelCompressor.compress_model(model=model,\n",
    "                                                              eval_callback=eval_callback,\n",
    "                                                              eval_iterations=eval_iterations,\n",
    "                                                              input_shape=(1, 3, 224, 224),\n",
    "                                                              compress_scheme=compress_scheme,\n",
    "                                                              cost_metric=cost_metric,\n",
    "                                                              parameters=params)\n",
    "\n",
    "print(comp_stats)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "The QuantizationSim model is now ready to be used for inference or training. \n",
    "\n",
    "**3.3 Pass the model to the same evaluation routine as before to calculate a simulated quantized accuracy score for the compressed model.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "accuracy = ImageNetDataPipeline.evaluate(compressed_model, iterations=None, use_cuda=use_cuda)\n",
    "print(accuracy)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "Model accuracy falls sharply after compression. This is expected. Fine-tuning is used to recover accuracy."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**3.4. Fine-tune the model.**\n",
    "\n",
    "As with any training job, hyper-parameters need to be searched for optimal results. Good starting points are to use a learning rate on the same order as the ending learning rate when training the original model, and to drop the learning rate by a factor of 10 every 5 epochs or so.\n",
    "\n",
    "This example trains for only one epoch, but you can experiment with the parameters however you like."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ImageNetDataPipeline.finetune(compressed_model, epochs=2, learning_rate=15e-4, learning_rate_schedule=[5, 10],\n",
    "                              use_cuda=use_cuda)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "**3.5 After fine-tuning, evaluate the model again to see the improvements in accuracy.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "accuracy = ImageNetDataPipeline.evaluate(compressed_model, iterations=None, use_cuda=use_cuda)\n",
    "print(accuracy)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Of course, there might be little gain in accuracy after only one epoch of training. Experiment with the hyper-parameters to get better results.\n",
    "\n",
    "## Next steps\n",
    "\n",
    "The next step is to save the model.\n",
    "\n",
    "**Save the fine-tuned model.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "os.makedirs('./output/', exist_ok=True)\n",
    "torch.save(compressed_model, './output/finetuned_model')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## For more information\n",
    "\n",
    "See the [AIMET API docs](https://quic.github.io/aimet-pages/AimetDocs/api_docs/index.html) for details about the AIMET APIs and optional parameters.\n",
    "\n",
    "See the [other example notebooks](https://github.com/quic/aimet/tree/develop/Examples/torch/compression) to learn how to use other AIMET compression techniques."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
